API Loading and Error Handling in Flutter
When a Flutter application communicates with an API, the request may take some time to complete, fail because of network problems, return an unexpected status code, or provide invalid data. A good Flutter application should clearly handle loading, success, empty, and error states so that users always understand what is happening.
Flutter commonly uses the http package for HTTP requests and FutureBuilder for displaying asynchronous loading, success, and error states.
1. What is API Loading?
API loading is the state displayed while Flutter is waiting for a response from a server. Because network requests are asynchronous, the application should not appear frozen while waiting for the response.
Typical loading indicators include:
CircularProgressIndicator
LinearProgressIndicator
- Loading text
- Skeleton screens
- Placeholder cards
2. Why is Loading Handling Important?
- It informs the user that the request is in progress.
- It prevents users from thinking that the application has stopped responding.
- It improves the overall user experience.
- It provides feedback during slow network connections.
- It makes asynchronous API operations easier to understand.
3. Basic API Request
First, add the http package to your Flutter project:
flutter pub add http
Import the package:
import 'package:http/http.dart' as http;
The http package provides a simple way to make HTTP requests from Flutter applications.
4. Understanding Future in API Calls
An HTTP request does not normally return its final result immediately. Dart represents asynchronous operations using Future.
Future<http.Response> fetchData() {
return http.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
);
}
A Future represents a value or error that becomes available later.
5. Understanding async and await
The async and await keywords make asynchronous API code easier to read.
Future<String> fetchData() async {
final response = await http.get(
Uri.parse('https://example.com/api/data'),
);
return response.body;
}
The await keyword waits for the Future to complete without blocking the application UI thread.
6. Basic Loading with FutureBuilder
FutureBuilder is useful for building widgets based on the current state of a Future. A common pattern is to show a loading indicator while waiting, display an error when snapshot.hasError is true, and display data when snapshot.hasData is true.
FutureBuilder<String>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.hasData) {
return Text(snapshot.data!);
}
return const Text('No data available');
},
)
7. FutureBuilder Connection States
FutureBuilder provides a connectionState that can be used to determine the current stage of an asynchronous operation.
| Connection State |
Meaning |
Typical UI |
| none |
No asynchronous operation is currently connected. |
Initial or empty UI |
| waiting |
The Future is still running. |
Loading indicator |
| active |
The asynchronous source is active. |
Progress UI where applicable |
| done |
The Future has completed. |
Success or error UI |
8. Checking ConnectionState
FutureBuilder<List<String>>(
future: fetchItems(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (snapshot.hasData) {
final items = snapshot.data!;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
);
}
return const Center(
child: Text('No data available'),
);
},
)
9. Complete API Loading Example
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Map<String, dynamic>> fetchUser() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
);
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
}
throw Exception('Failed to load user');
}
class UserPage extends StatefulWidget {
const UserPage({super.key});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
late Future<Map<String, dynamic>> userFuture;
@override
void initState() {
super.initState();
userFuture = fetchUser();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Details'),
),
body: FutureBuilder<Map<String, dynamic>>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
if (snapshot.hasData) {
final user = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user['name'] as String,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(user['email'] as String),
],
),
);
}
return const Center(
child: Text('No user data found'),
);
},
),
);
}
}
10. Why Should API Calls Not Be Made Inside build()?
The build() method can execute many times during the lifetime of a widget. Starting an API request directly inside build() can therefore cause repeated requests and unnecessary network traffic.
A common approach is to initialize the Future in initState() and then provide that Future to FutureBuilder.
late Future<Map<String, dynamic>> userFuture;
@override
void initState() {
super.initState();
userFuture = fetchUser();
}
11. What is API Error Handling?
API error handling means detecting and responding to problems that occur while communicating with a server.
Errors can happen because of:
- No internet connection
- Slow or unstable network
- Server errors
- Invalid API URLs
- Authentication failures
- Invalid request parameters
- Unexpected response data
- JSON parsing problems
- Request timeouts
12. HTTP Status Codes
An API response contains an HTTP status code. Your application should inspect the status code before treating the response as successful.
| Status Code |
Meaning |
Typical Flutter Response |
| 200 |
OK |
Process response |
| 201 |
Created |
Data successfully created |
| 400 |
Bad Request |
Show request validation message |
| 401 |
Unauthorized |
Ask user to authenticate |
| 403 |
Forbidden |
Show permission message |
| 404 |
Not Found |
Show resource-not-found message |
| 500 |
Internal Server Error |
Show server error message |
| 503 |
Service Unavailable |
Ask user to retry later |
13. Checking the Status Code
Future<String> fetchData() async {
final response = await http.get(
Uri.parse('https://example.com/api/data'),
);
if (response.statusCode == 200) {
return response.body;
}
throw Exception(
'Request failed with status code: ${response.statusCode}',
);
}
14. Using try-catch for Network Errors
The try-catch statement can be used to catch exceptions generated during asynchronous operations.
Future<String> fetchData() async {
try {
final response = await http.get(
Uri.parse('https://example.com/api/data'),
);
if (response.statusCode == 200) {
return response.body;
}
throw Exception(
'Server returned ${response.statusCode}',
);
} catch (e) {
throw Exception('Network request failed: $e');
}
}
15. Displaying a User-Friendly Error
Technical error messages are useful for developers, but users generally need simple and understandable messages.
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 50,
),
const SizedBox(height: 12),
const Text(
'Unable to load data.',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: retry,
child: const Text('Try Again'),
),
],
),
);
}
16. Retry API Requests
A retry button allows the user to execute the request again after an error.
class UserPage extends StatefulWidget {
const UserPage({super.key});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
late Future<Map<String, dynamic>> userFuture;
@override
void initState() {
super.initState();
loadUser();
}
void loadUser() {
userFuture = fetchUser();
}
void retry() {
setState(() {
loadUser();
});
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, dynamic>>(
future: userFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: ElevatedButton(
onPressed: retry,
child: const Text('Try Again'),
),
);
}
if (snapshot.hasData) {
return Text(snapshot.data!['name'] as String);
}
return const Text('No data');
},
);
}
}
17. Loading, Success, Empty, and Error States
A robust API screen commonly handles four major states:
| State |
Condition |
UI Example |
| Loading |
Request is running |
Progress indicator |
| Success |
Valid data received |
Display API data |
| Empty |
Request succeeds but no records exist |
No data message |
| Error |
Request or parsing fails |
Error message + retry |
18. Handling an Empty List
if (snapshot.hasData) {
final users = snapshot.data!;
if (users.isEmpty) {
return const Center(
child: Text('No users found'),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
);
}
19. Complete Loading and Error UI
FutureBuilder<List<User>>(
future: usersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Something went wrong.'),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
setState(() {
usersFuture = fetchUsers();
});
},
child: const Text('Retry'),
),
],
),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(
child: Text('No users available.'),
);
}
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
);
},
)
20. Handling JSON Parsing Errors
An API may return data that does not match the expected JSON structure. Model parsing code should detect invalid data and throw an appropriate exception.
class User {
final int id;
final String name;
User({
required this.id,
required this.name,
});
factory User.fromJson(Map<String, dynamic> json) {
if (json['id'] is! int || json['name'] is! String) {
throw const FormatException(
'Invalid user data',
);
}
return User(
id: json['id'] as int,
name: json['name'] as String,
);
}
}
21. Handling Different Error Types
It is useful to distinguish between different failures so that the application can provide more meaningful messages.
Future<List<User>> fetchUsers() async {
try {
final response = await http.get(
Uri.parse('https://example.com/api/users'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data is! List) {
throw const FormatException(
'Invalid response format',
);
}
return data
.map((item) => User.fromJson(
item as Map<String, dynamic>,
))
.toList();
}
if (response.statusCode == 401) {
throw Exception('Authentication required');
}
if (response.statusCode == 404) {
throw Exception('Users endpoint not found');
}
if (response.statusCode >= 500) {
throw Exception('Server error');
}
throw Exception(
'Request failed: ${response.statusCode}',
);
} on FormatException {
throw Exception('Invalid data received from server');
} catch (e) {
throw Exception('Unable to connect to server');
}
}
22. Showing Different Messages for Different Errors
String getErrorMessage(Object? error) {
final message = error.toString();
if (message.contains('Authentication')) {
return 'Please log in again.';
}
if (message.contains('Server')) {
return 'The server is currently unavailable.';
}
if (message.contains('Invalid')) {
return 'The server returned invalid data.';
}
return 'Please check your internet connection and try again.';
}
23. Using LinearProgressIndicator
A linear progress indicator can be useful when the loading state should appear at the top of a page.
Column(
children: [
const LinearProgressIndicator(),
Expanded(
child: ListView(
children: const [
ListTile(
title: Text('Loading...'),
),
],
),
),
],
)
24. Using a Loading Overlay
For operations such as submitting forms, a loading overlay can prevent users from repeatedly pressing a button.
Stack(
children: [
Form(
child: Column(
children: [
TextFormField(),
ElevatedButton(
onPressed: isLoading ? null : submitForm,
child: const Text('Submit'),
),
],
),
),
if (isLoading)
Container(
color: Colors.black26,
child: const Center(
child: CircularProgressIndicator(),
),
),
],
)
25. Preventing Multiple API Requests
Users may accidentally tap a button multiple times while a request is running. Disable the action while loading.
ElevatedButton(
onPressed: isLoading ? null : submitForm,
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
)
: const Text('Submit'),
)
26. Refreshing API Data
API data can be refreshed by assigning a new Future and rebuilding the widget.
Future<void> refreshUsers() async {
setState(() {
usersFuture = fetchUsers();
});
}
27. Pull-to-Refresh
RefreshIndicator provides a familiar pull-to-refresh interaction for scrollable content.
RefreshIndicator(
onRefresh: () async {
setState(() {
usersFuture = fetchUsers();
});
await usersFuture;
},
child: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
),
)
28. API Timeout Handling
A timeout can prevent an application from waiting indefinitely for a server response.
Future<http.Response> fetchData() async {
return await http
.get(
Uri.parse('https://example.com/api/data'),
)
.timeout(
const Duration(seconds: 10),
);
}
The timeout can be caught and converted into a user-friendly message.
try {
final response = await fetchData();
return response;
} on TimeoutException {
throw Exception(
'The request took too long. Please try again.',
);
}
29. Complete API Service Example
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
static const String baseUrl =
'https://jsonplaceholder.typicode.com';
Future<List<Map<String, dynamic>>> fetchUsers() async {
try {
final response = await http
.get(
Uri.parse('$baseUrl/users'),
)
.timeout(
const Duration(seconds: 10),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data is! List) {
throw const FormatException(
'Invalid response format',
);
}
return List<Map<String, dynamic>>.from(
data.map(
(item) => Map<String, dynamic>.from(item as Map),
),
);
}
throw Exception(
'Server returned ${response.statusCode}',
);
} on TimeoutException {
throw Exception('Request timed out');
} on FormatException {
throw Exception('Invalid data received');
} catch (e) {
throw Exception('Unable to fetch users');
}
}
}
30. API Service with Flutter UI
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
@override
State<UsersPage> createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
final ApiService apiService = ApiService();
late Future<List<Map<String, dynamic>>> usersFuture;
@override
void initState() {
super.initState();
usersFuture = apiService.fetchUsers();
}
void retry() {
setState(() {
usersFuture = apiService.fetchUsers();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Users'),
),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: usersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Unable to load users.',
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: retry,
child: const Text('Retry'),
),
],
),
);
}
final users = snapshot.data ?? [];
if (users.isEmpty) {
return const Center(
child: Text('No users found.'),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user['name'] as String),
subtitle: Text(user['email'] as String),
);
},
);
},
),
);
}
}
31. Error Handling Flow
User opens screen
|
v
Start API request
|
v
Show Loading UI
|
v
Receive response
|
+--------------------+
| |
v v
Success Error
| |
v v
Parse JSON Identify error
| |
v v
Check data Show message
| |
v v
Display UI Retry option
32. Common API Errors and Solutions
| Problem |
Possible Cause |
Solution |
| Loading forever |
Future never completes or result is not handled correctly |
Handle success and error states and ensure the Future completes |
| 404 error |
Incorrect endpoint or resource |
Check API URL and resource ID |
| 401 error |
Authentication missing or expired |
Authenticate or refresh credentials |
| 500 error |
Server-side problem |
Display a server error and provide retry |
| JSON parsing error |
Unexpected response structure |
Validate the response before parsing |
| Timeout |
Slow server or network |
Set a timeout and allow retry |
| Repeated requests |
API called from build() |
Store the Future in state |
| App appears frozen |
No loading feedback |
Display a progress indicator |
33. Common Mistakes to Avoid
- Do not start API requests repeatedly inside
build().
- Do not assume every HTTP response is successful.
- Do not display only a spinner without an error state.
- Do not display confusing technical error messages directly to users.
- Do not ignore JSON parsing failures.
- Do not allow users to repeatedly submit the same request while it is loading.
- Do not leave an API screen without an empty-data state.
- Do not ignore timeout and network failures.
- Do not assume that a successful HTTP request always means useful application data was returned.
34. Best Practices for API Loading and Error Handling
- Keep API code separate from UI code when possible.
- Use
Future and async/await for asynchronous operations.
- Check HTTP status codes.
- Use
try-catch for exceptions.
- Display a clear loading state.
- Display a meaningful error state.
- Provide a retry action when appropriate.
- Handle empty API responses.
- Validate JSON before converting it to models.
- Avoid making API calls directly inside
build().
- Use timeouts for requests that should not wait indefinitely.
- Disable duplicate actions while a request is in progress.
- Keep technical error details in logs while showing simple messages to users.
35. Loading and Error Handling with a Custom State
For larger applications, a custom state can make the UI easier to maintain.
enum ApiState {
loading,
success,
empty,
error,
}
ApiState state = ApiState.loading;
The UI can then be selected according to the current state:
switch (state) {
case ApiState.loading:
return const CircularProgressIndicator();
case ApiState.success:
return const Text(
'Data loaded successfully',
);
case ApiState.empty:
return const Text(
'No data available',
);
case ApiState.error:
return ElevatedButton(
onPressed: retry,
child: const Text('Retry'),
);
}
36. API Loading State Example
bool isLoading = false;
Future<void> loadData() async {
setState(() {
isLoading = true;
});
try {
final data = await fetchData();
// Process data here.
} catch (e) {
// Handle error here.
} finally {
if (mounted) {
setState(() {
isLoading = false;
});
}
}
}
37. Why finally is Useful
The finally block is useful when an operation must be performed regardless of whether the request succeeds or fails. For example, a loading flag should normally be reset after both successful and failed requests.
38. Error Handling at the Application Level
Flutter also provides mechanisms for handling framework-level errors. FlutterError.onError can handle errors routed through Flutter's framework callbacks, while asynchronous errors outside those callbacks can be handled using PlatformDispatcher.instance.onError.
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
FlutterError.onError = (details) {
FlutterError.presentError(details);
};
PlatformDispatcher.instance.onError = (error, stack) {
debugPrint('Unhandled error: $error');
return true;
};
runApp(const MyApp());
}
39. API Loading and Error Handling Architecture
Flutter UI
|
v
View / Screen
|
v
ViewModel / Controller
|
v
API Service
|
v
HTTP Client
|
v
Backend API
|
v
Response
|
+------ Success ------> Model ------> UI
|
+------ Error --------> Exception ---> UI Error State
40. Practical Example: Product List
Suppose an application loads products from an API. The screen should handle all major states:
FutureBuilder<List<Product>>(
future: productFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Unable to load products.'),
ElevatedButton(
onPressed: retry,
child: const Text('Retry'),
),
],
),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(
child: Text('No products available.'),
);
}
final products = snapshot.data!;
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text(product.price.toString()),
),
);
},
);
},
)
41. Practical API Loading and Error Handling Example
A practical application should combine loading, success, empty, error, retry, and refresh behavior into a single predictable flow.
class ProductsPage extends StatefulWidget {
const ProductsPage({super.key});
@override
State<ProductsPage> createState() => _ProductsPageState();
}
class _ProductsPageState extends State<ProductsPage> {
late Future<List<Product>> productsFuture;
@override
void initState() {
super.initState();
productsFuture = fetchProducts();
}
void retry() {
setState(() {
productsFuture = fetchProducts();
});
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Product>>(
future: productsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Unable to load products.'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: retry,
child: const Text('Retry'),
),
],
),
);
}
final products = snapshot.data ?? [];
if (products.isEmpty) {
return const Center(
child: Text('No products available.'),
);
}
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text(
product.price.toString(),
),
),
);
},
);
},
);
}
}
42. API Error Handling Checklist
- Show loading feedback before or while the request is running.
- Check the HTTP status code.
- Parse JSON safely.
- Catch network exceptions.
- Handle timeouts.
- Handle empty responses.
- Show a user-friendly error message.
- Provide retry functionality.
- Prevent duplicate requests.
- Do not call APIs repeatedly from
build().
- Keep API and UI responsibilities separated.
43. Recommended Development Process
- Create an API service method.
- Define the expected response model.
- Make the HTTP request using
http.
- Check the response status code.
- Decode and validate the JSON response.
- Convert JSON into Dart models.
- Expose the API result as a Future.
- Store the Future outside the
build() method.
- Display a loading state.
- Display the successful response.
- Handle empty data.
- Handle exceptions and errors.
- Add retry functionality where appropriate.
- Add timeout and refresh behavior where required.
44. Interview Questions
- What is API loading in Flutter?
- Why do Flutter applications need loading indicators?
- What is a Future in Dart?
- What is FutureBuilder?
- How do you check whether a Future is still loading?
- What is the difference between
snapshot.hasData and snapshot.hasError?
- Why should API calls generally not be placed directly inside
build()?
- How do you handle HTTP status codes?
- How does
try-catch help with API requests?
- How can you implement a retry button?
- How can you handle an empty API response?
- How do you handle JSON parsing errors?
- How can you implement request timeout handling?
- How can you prevent duplicate API requests?
- What is the purpose of
finally in an API request?
- What is the difference between loading, success, empty, and error states?
- Why should technical error messages generally not be shown directly to end users?
45. Practice Exercise
Create a Flutter application that loads a list of users from an API.
- Create a Flutter project.
- Add the
http package.
- Create an API service class.
- Make a GET request.
- Check the HTTP status code.
- Parse the JSON response.
- Create a User model.
- Display a loading indicator while the request is running.
- Display users when the request succeeds.
- Display an empty message when no users are returned.
- Display an error message when the request fails.
- Add a Retry button.
- Add pull-to-refresh functionality.
- Add timeout handling.
- Disable duplicate requests where necessary.
46. Quick Revision
| Concept |
Purpose |
| Future |
Represents an asynchronous result |
| async |
Marks a function as asynchronous |
| await |
Waits for a Future to complete |
| FutureBuilder |
Builds UI according to a Future's state |
| ConnectionState.waiting |
Indicates that the Future is still running |
| snapshot.hasData |
Checks whether non-null data is available |
| snapshot.hasError |
Checks whether the Future completed with an error |
| try-catch |
Catches exceptions |
| statusCode |
Indicates the HTTP response status |
| RefreshIndicator |
Provides pull-to-refresh behavior |
| Timeout |
Stops waiting after a specified duration |
| Retry |
Allows the user to execute a failed request again |
| Empty State |
Communicates that the request succeeded but no records are available |
47. Learning Outcome
After completing this topic, you should be able to:
- Understand asynchronous API loading in Flutter.
- Use
Future, async, and await.
- Use
FutureBuilder to manage asynchronous UI states.
- Handle loading, success, empty, and error states.
- Check HTTP status codes.
- Handle network and parsing exceptions.
- Implement retry functionality.
- Implement pull-to-refresh.
- Handle request timeouts.
- Prevent duplicate API requests.
- Separate API services from UI code.
- Build more reliable API-driven Flutter applications.
48. Summary
API loading and error handling are essential parts of Flutter application development. A production-quality application should never assume that every network request succeeds. The UI should clearly communicate when data is loading, when data has been successfully received, when no data is available, and when an error has occurred.
Using http, Future, async/await, try-catch, status-code validation, and FutureBuilder provides a strong foundation for handling API-based Flutter applications.
Core flow:
API Request
↓
Loading State
↓
Receive Response
↓
Check Status Code
↓
Parse JSON
↓
Validate Data
↓
+-------------------------------+
| |
v v
Success Error
| |
v v
Display Data Show Error
| |
v v
Empty Check Retry
|
v
Empty / Data UI
49. Useful Resources
50. JustAcademy Flutter Training
Learn Flutter development with structured practical training, including Flutter development, Dart programming, API integration, Firebase, UI development, and application development.
Explore JustAcademy Flutter Training Course
Register for Flutter Course Demo